/*
 * GDefaultLeagueImpl.java
 * Created on 14 juil. 2007
 *
 * Glacéo Internet Platform
 * http://sourceforge.net/projects/chm/
 *
 * Copyright (c) 2005-2007, Jan Janke (VirtualHockey Project)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.hockeo.vhbase.data.impl.contest;

import java.math.BigDecimal;
import java.util.List;

import org.springframework.beans.factory.annotation.Required;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import com.hockeo.vhbase.conf.GRulesConstants;
import com.hockeo.vhbase.data.dao.IGDao;
import com.hockeo.vhbase.data.dao.club.GClub;
import com.hockeo.vhbase.data.dao.club.GClubLeagueHistoryRecord;
import com.hockeo.vhbase.data.dao.contest.GContest;
import com.hockeo.vhbase.data.dao.contest.GContestElement;
import com.hockeo.vhbase.data.dao.contest.GContestHistoryRecord;
import com.hockeo.vhbase.data.dao.contest.GContestRound;
import com.hockeo.vhbase.data.dao.contest.GLeague;
import com.hockeo.vhbase.data.dao.contest.GRoundRobinContestElement;
import com.hockeo.vhbase.data.dao.season.GMatchday;
import com.hockeo.vhbase.data.dao.season.GMoveday;
import com.hockeo.vhbase.data.dao.season.GSeason;
import com.hockeo.vhbase.data.dao.standings.GArchivedStandings;
import com.hockeo.vhbase.data.dao.standings.GGoalsAccount;
import com.hockeo.vhbase.data.dao.standings.GStandings;
import com.hockeo.vhbase.data.helper.GArchivedStandingsType;
import com.hockeo.vhbase.data.helper.GContestRoundFormat;
import com.hockeo.vhbase.data.helper.GContestRoundType;
import com.hockeo.vhbase.data.helper.GContestUtils;
import com.hockeo.vhbase.data.helper.GStandingsType;
import com.hockeo.vhbase.data.impl.IGContestImpl;
import com.hockeo.vhbase.data.impl.goals.IGGoalsDistributor;
import com.hockeo.vhbase.error.GDataException;
import com.hockeo.vhbase.error.GException;
import com.hockeo.vhbase.seasoncycle.services.GRoundRobinMatchCreator;

/**
 * Default implementation for all the actions to be carried out for national hockey
 * leagues.
 *
 * @version $Id$
 * @author jjanke
 */
@Transactional( propagation = Propagation.MANDATORY, rollbackFor = { GException.class } )
public class GDefaultLeagueImpl implements IGContestImpl
{
  private IGDao                   d_dao;
  private GRoundRobinMatchCreator d_rrMatchGenerator;
  private IGGoalsDistributor      d_goalsDistributor;

  @Required
  public final void setDao( IGDao dao )
  {
    d_dao = dao;
  }

  @Required
  public void setGoalsDistributor( IGGoalsDistributor goalsDistributor )
  {
    d_goalsDistributor = goalsDistributor;
  }

  /**
   * Creates a standard league structure (one round robin contest element) if no contest
   * round exists so far.
   */
  public void createStructure( final GSeason season, final GContest contest ) throws GDataException
  {
    GLeague league = d_dao.findEntity( GLeague.class, contest.getId() );

    // Check if contest structure exists. If at least one contest round exists, it's ok
    // and we can stop.
    //
    if ( !league.getContestRounds().isEmpty() )
      return;

    // Otherwise, let's create the contest round and round robin element
    //
    GContestRound round = new GContestRound( league, league.getId() + "-1", 1, GContestRoundType.ROUND_MAIN,
        GContestRoundFormat.ROUND_ROBIN, false );
    GRoundRobinContestElement element = new GRoundRobinContestElement( round, round.getId() + "-1", "A",
        league.getNumTeams(), league.getNumRounds(), league.getNumPromoted1(), league.getNumPromoted2(),
        league.getNumRelegated() );

    round.addContestElement( element );
    round.setLastRound( true );

    d_dao.makePersistent( round );
  }

  /**
   * This method has an empty implementation and does nothing because the structure for a
   * default league - once created - needs to be maintained manually. The automatic
   * structure creation carried out {@link #createStructure(GSeason, GContest)} is only a
   * one-time service for newly created leagues.
   */
  public void undoCreateStructure( GSeason season, GContest contest ) throws GDataException
  {
    // Just reset 'started', 'played' and 'analysed' flags of all contest rounds.
    //
    for ( GContestRound round : contest )
      round.reset();
  }

  /**
   * This method has an empty implementation and does nothing because the structure for a
   * default league needs to be maintained manually.
   */
  public void deleteStructure( final GSeason season, final GContest contest ) throws GDataException
  {
    // Just reset 'started', 'played' and 'analysed' flags of all contest rounds.
    //
    for ( GContestRound round : contest )
      round.reset();
  }

  /*
   * @see
   * com.hockeo.vhbase.data.impl.IGContestImpl#createMatchdays(com.hockeo.vhbase.data.
   * GContest)
   */
  public void createMatchdays( GContest contest ) throws GDataException
  {
    assert d_dao != null : "No DAO has been set.";

    // A default league has one or more RoundRobinContestElements. Get them and create
    // the matchdays for all of them.
    //
    for ( GContestRound round : contest.getContestRounds() )
    {
      for ( GContestElement element : round.getContestElements() )
      {
        int nNumMatchdays = element.getNumMatchdays();
        for ( int i = 1; i <= nNumMatchdays; i++ )
        {
          GMatchday matchday = new GMatchday( element, i, i == 1, i == nNumMatchdays );

          d_dao.makePersistent( matchday );
        }
      }
    }
  }

  /*
   * @see
   * com.hockeo.vhbase.data.impl.IGContestImpl#assignMatchdaysToMovedays(com.hockeo.vhbase
   * .data.GContest)
   */
  public void assignMatchdaysToMovedays( GContest contest ) throws GDataException
  {
    assert d_dao != null : "No DAO has been set.";

    /*
     * It is assumed that a league has a single round robin contest element which is
     * responsible for distributing all matchdays correctly to the available movedays.
     */
    if ( contest.getContestRounds().size() != 1 )
      throw new GDataException( "Assigning matchdays to leagues with more than one contest round is not supported."
          + "\nE.g. the league " + contest.getId() + " has more than one attached round." );

    List<GMoveday> listAvailableMovedays = d_dao.findReservedMovedays( contest );

    for ( GContestElement element : contest.getContestRounds().get( 0 ) )
    {
      if ( !element.instanceOf( GRoundRobinContestElement.class ) )
        throw new GDataException( "Assigning matchdays to leagues only works with round robin contest elements."
            + "\nE.g. the contest element " + element.getId() + " is of type " + element.getClass().getName() );

      element.attachMatchdaysToMovedays( listAvailableMovedays, d_dao.findMatchdays( element ), false );
    }
  }

  /**
   * Creates all matches of the standard round robin contest elements attached to the
   * single contest round of the league passed in as argument.
   */
  public void createNextContestRound( final GContest contest, final GSeason season, final GMoveday movedayCurrent )
      throws GDataException
  {
    // Only if the next moveday is the first one of the season, something needs to be done
    // for leagues
    //
    if ( movedayCurrent.getId() != GRulesConstants.ID_FIRST_MOVEDAY_OF_SEASON )
      return;

    GLeague league = d_dao.findEntity( GLeague.class, contest.getId() );
    GRoundRobinContestElement element = league.getRegularSeasonElement();

    createGoalAccounts( element );

    if ( d_rrMatchGenerator == null )
      d_rrMatchGenerator = new GRoundRobinMatchCreator( d_dao );

    GContestImplUtils.createMatches( d_dao, element, d_rrMatchGenerator );
  }

  /**
   * Removes all goal accounts and standings if the given moveday is the first of the
   * season.
   *
   * @see com.hockeo.vhbase.data.impl.IGContestImpl#undoCreateNextContestRound(com.hockeo.vhbase.data.dao.contest.GContest,
   *      com.hockeo.vhbase.data.dao.season.GMoveday)
   */
  public void undoCreateNextContestRound( final GContest contest, final GMoveday movedayLastPrepared )
      throws GDataException
  {
    // Only if the last prepared moveday is the first one of the season, something needs
    // to be done for leagues
    //
    if ( movedayLastPrepared.getId() != GRulesConstants.ID_FIRST_MOVEDAY_OF_SEASON )
      return;

    GLeague league = d_dao.findEntity( GLeague.class, contest.getId() );
    d_dao.deleteGoalAccountsMatchesFreeTicketsAndStandings( league.getRegularSeasonElement() );
  }

  /**
   * Resets the goal account so that it points to the value of the last standings before
   * the moveday.
   *
   * @see com.hockeo.vhbase.data.impl.IGContestImpl#resetGoalAccounts(com.hockeo.vhbase.data.dao.season.GMoveday,
   *      com.hockeo.vhbase.data.dao.contest.GContestElement)
   */
  public void resetGoalAccounts( final GMoveday moveday, final GContestElement element ) throws GDataException
  {
    GContestImplUtils.resetGoalAccounts( d_dao, moveday, d_dao.findEntity( GRoundRobinContestElement.class,
        element.getId() ) );
  }

  /*
   * @see
   * com.hockeo.vhbase.data.impl.IGContestImpl#resetContestStats(com.hockeo.vhbase.data
   * .GMoveday, com.hockeo.vhbase.data.GContestElement)
   */
  public void resetContestStats( GMoveday moveday, GContestElement element ) throws GDataException
  {
  // In the moment there is nothing to reset.
  //
  }

  /*
   * This method requires a new transaction because it needs to access standings that
   * belong to a previous matchday being part of the same moveday. Since all matchdays of
   * a specific moveday are analysed in the same time, it must be made sure that standings
   * for the same moveday previously created are already readable by this method although
   * it is part of the same global transaction.
   *
   * @seecom.hockeo.vhbase.data.impl.IGContestImpl#analyseMatches(com.hockeo.vhbase.data.
   * GContestElement, com.hockeo.vhbase.data.GMatchday)
   */
  @Transactional( propagation = Propagation.REQUIRES_NEW, rollbackFor = { GException.class } )
  public void analyseMatches( final GContestElement contestElement, final GMatchday matchday ) throws GDataException
  {
    final GRoundRobinContestElement element = d_dao.findEntity( GRoundRobinContestElement.class, contestElement.getId() );

    GContestImplUtils.analyseRRMatchday( d_dao, element, matchday );
  }

  public void postprocessMultiElementRound( GContestRound round, Integer nMatchday ) throws GDataException
  {
  // Currently, there are no leagues with more than 1 contest element.
  }

  /**
   * For leagues, we have to archive the standings, update the eternal standings and
   * create a club/league history record.
   *
   * @see com.hockeo.vhbase.data.impl.IGContestImpl#createEndOfSeasonStatistics(GContestElement
   *      )
   */
  @Transactional( propagation = Propagation.REQUIRES_NEW, rollbackFor = { GException.class } )
  public void createEndOfSeasonStatistics( final GContestElement contestElement ) throws GDataException
  {
    GSeason season = d_dao.findCurrentSeason();
    GContest contest = contestElement.getContest();
    GRoundRobinContestElement element = d_dao.findEntity( GRoundRobinContestElement.class, contestElement.getId() );

    // Make sure, that no archived standings exist already
    //
    if ( d_dao.findNumArchivedStandings( GArchivedStandingsType.END_OF_SEASON, season, contestElement ) > 0 )
      return;

    List<GStandings> listStandings = d_dao.findFinalStandings( element );

    // Archived standings
    //
    for ( GStandings s : listStandings )
    {
      GArchivedStandings as;

      as = new GArchivedStandings( GArchivedStandingsType.END_OF_SEASON, season, element, s );
      d_dao.makePersistent( as );

      // For the total standings, we also have to create a club/league history record
      //
      if ( s.getType() == GStandingsType.TOTAL )
      {
        GClubLeagueHistoryRecord clhr = new GClubLeagueHistoryRecord( as );
        d_dao.makePersistent( clhr );
      }
    }

    // Create eternal standings
    //
    d_dao.createEternalStandings( season, element );

    // Contest history
    //
    GContestHistoryRecord chr = new GContestHistoryRecord( contest.getHistoryType(), season, contest,
        listStandings.get( 0 ).getClub(), listStandings.get( 1 ).getClub() );

    d_dao.makePersistent( chr );
  }

  public void deleteEndOfSeasonStatistics( final GContestElement contestElement ) throws GDataException
  {
    GSeason season = d_dao.findCurrentSeason();

    // Contest history
    //
    GContestHistoryRecord chr = d_dao.findContestHistoryRecord( season, contestElement.getContest() );
    if ( chr != null )
      d_dao.delete( chr );

    // Club/league history entries
    //
    d_dao.deleteClubHistoryRecords( season, contestElement.getContestRound().getContest() );

    // Archived season standings
    //
    d_dao.deleteArchivedStandings( season, contestElement );

    // Recreate eternal league standings (based on previous season)
    //
    d_dao.createEternalStandings( season, contestElement );
  }

  /**
   * Creates the goal accounts for all clubs that participate in the given contest
   * element.
   *
   * @param element the contest element for which to create goal accounts
   * @throws GDataException
   */
  private void createGoalAccounts( GRoundRobinContestElement element ) throws GDataException
  {
    // Check if goal accounts have already been created. If yes, we can stop right away.
    //
    if ( d_dao.findNumGoalsAccounts( element ) > 0 )
      return;

    // Get all clubs and determine the number of initial goals to attribute
    //
    GSeason season = d_dao.findCurrentSeason();
    GLeague league = d_dao.findEntity( GLeague.class, element.getContestRound().getContest().getId() );
    BigDecimal[] adecGoals = GContestUtils.getInitialGoalsRoundRobin( league.getNumTeams(), league.getNumRounds(),
        0.125 );

    // Check if this is the first ever season
    //
    boolean fFirstSeason = element.getContest().isFirstSeason( season );

    // Now create goal accounts
    //
    int iGoals = 0;
    int nLastSeeding = 0;
    for ( int i = 0; i < league.getClubs().size(); i++ )
    {
      GGoalsAccount goalsAccount;

      // The list of clubs is ordered ascendingly by the seeding position
      //
      GClub club = league.getClubs().get( i );

      if ( fFirstSeason )
        goalsAccount = new GGoalsAccount( club, element, adecGoals[0] );
      else
      {
        if ( nLastSeeding == 0 || nLastSeeding < club.getSeeding() )
        {
          iGoals++;
          nLastSeeding = club.getSeeding();
          goalsAccount = new GGoalsAccount( club, element, adecGoals[iGoals] );
        }
        else
        {
          // If the seeding is equal to the one of the previous club, we assign
          // the same number of goals.
          //
          goalsAccount = new GGoalsAccount( club, element, adecGoals[iGoals] );
        }
      }

      d_dao.makePersistent( goalsAccount );
    }
  }
}
